{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Chess App"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp chess_app"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "! uv pip install chess stockfish"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Todos\n",
    "- ~~Use websockets to alert player when another one has joined (opened a second browser and connected)~~\n",
    "- ~~Use websockets to alert player when another one has left (closed the browser)~~\n",
    "- ~~Use websockets to alert player when another one has made a move~~\n",
    "- Allow capturing of pieces, handling checks, draws, and checkmates\n",
    "- Add evaluation bar via stockfish chess engine to show who is winning"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from fasthtml.common import *\n",
    "from fastcore.utils import *\n",
    "from fastcore.xml import to_xml\n",
    "from starlette.endpoints import WebSocketEndpoint\n",
    "from starlette.routing import WebSocketRoute\n",
    "\n",
    "import chess\n",
    "import chess.svg\n",
    "\n",
    "cboard = chess.Board()\n",
    "# move e2e4\n",
    "cboard.push_san('e4')\n",
    "cboard.push_san('e5')\n",
    "css = Style(\n",
    "    '''\\\n",
    "    #chess-board { display: grid; grid-template-columns: repeat(8, 64px); grid-template-rows: repeat(8, 64px);gap: 1px; }\n",
    "    .board-cell { width: 64px; height: 64px; border: 1px solid black; }\n",
    "    .black { background-color: grey; }\n",
    "    .white { background-color: white; }\n",
    "    .active { background-color: green; }\n",
    "    '''\n",
    ")\n",
    "# Flexbox CSS (http://flexboxgrid.com/)\n",
    "gridlink = Link(rel=\"stylesheet\", href=\"https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css\", type=\"text/css\")\n",
    "htmx_ws = Script(src=\"https://unpkg.com/htmx-ext-ws@2.0.0/ws.js\")\n",
    "\n",
    "app = FastHTML(hdrs=(gridlink, css, htmx_ws,))\n",
    "rt = app.route"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "player_queue = []\n",
    "class WS(WebSocketEndpoint):\n",
    "    encoding = 'text'\n",
    "\n",
    "    async def on_connect(self, websocket):\n",
    "        global player_queue\n",
    "        player_queue.append(websocket)\n",
    "        await websocket.accept()\n",
    "        print(f'There are {len(player_queue)} players in the queue')\n",
    "        await websocket.send_text(\"<div id='user-message'>Hello, you connected!</div>\")\n",
    "        if len(player_queue) == 2:\n",
    "            await player_queue[0].send_text(\"<div id='user-message'>Opponent joined! Let the game begin!</div>\")\n",
    "            await player_queue[1].send_text(\"<div id='user-message'>You joined! Let the game begin!</div>\")\n",
    "\n",
    "    async def on_receive(self, websocket, data):\n",
    "        await websocket.send_text(\"hi\")\n",
    "\n",
    "    async def on_disconnect(self, websocket, close_code):\n",
    "        global player_queue\n",
    "        player_queue.remove(websocket)\n",
    "        for player in player_queue:\n",
    "            await player.send_text(\"<div id='user-message'>Opponent disconnected!</div>\")\n",
    "\n",
    "app.routes.append(WebSocketRoute('/chess', WS))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "ROWS = '87654321'\n",
    "COLS = 'abcdefgh'\n",
    "def Board(lmoves: list[str] = [], selected: str = ''):\n",
    "    board = []\n",
    "    for row in ROWS:\n",
    "        board_row = []\n",
    "        for col in COLS:\n",
    "            pos = f\"{col}{row}\"\n",
    "            cell_color = \"black\" if (ROWS.index(row) + COLS.index(col)) % 2 == 0 else \"white\"\n",
    "            cell_color = 'active' if pos in lmoves else cell_color\n",
    "            cell_cls = f'board-cell {cell_color}'\n",
    "            if pos == selected:\n",
    "                cell_cls += ' selected'\n",
    "            piece = cboard.piece_at(chess.parse_square(pos))\n",
    "            if piece:\n",
    "                piece = NotStr(chess.svg.piece(piece))\n",
    "                board_row.append(\n",
    "                    Div(\n",
    "                        piece, id=pos, cls=cell_cls, hx_post=\"/select\", hx_vals={'col': col, 'row': row},\n",
    "                        hx_swap='outerHTML', hx_target='#chess-board', hx_trigger='click'\n",
    "                    )\n",
    "                )\n",
    "            else:\n",
    "                cell = Div(id=pos, cls=cell_cls)\n",
    "                if selected != '':\n",
    "                    move = f'{selected}{pos}'\n",
    "                    print(move)\n",
    "                    if chess.Move.from_uci(move) in cboard.legal_moves:\n",
    "                        cell = Div(id=pos, cls=cell_cls, hx_put=\"/move\", hx_vals={'move': move},\n",
    "                            hx_swap='outerHTML', hx_target='#chess-board', hx_trigger='click'\n",
    "                        )\n",
    "                board_row.append(cell)\n",
    "        board.append(Div(*board_row, cls=\"board-row\"))\n",
    "    return Div(*board, id=\"chess-board\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def Home():\n",
    "    return Div(\n",
    "        Div('Hello, still waiting on an opponent!', id='user-message'),\n",
    "        Board(),\n",
    "        hx_ext=\"ws\", ws_connect=\"/chess\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@rt(\"/\")\n",
    "def get():\n",
    "    return Home()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@rt('/select')\n",
    "async def post(col: str, row: str):\n",
    "    global cboards\n",
    "    lmoves = []\n",
    "    for m in cboard.legal_moves:\n",
    "        if str(m).startswith(f'{col}{row}'):\n",
    "            lmoves.append(str(m)[2:])\n",
    "    return Board(lmoves=lmoves, selected=f'{col}{row}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@rt('/move')\n",
    "async def put(move: str):\n",
    "    global cboards\n",
    "    cboard.push_san(move)\n",
    "    for player in player_queue:\n",
    "        await player.send_text(to_xml(Board()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| eval: false\n",
    "#| hide\n",
    "from nbdev.export import nb_export\n",
    "nb_export('chess_app.ipynb', '.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
